package org.tau.workshop2011.widgets

import collection.mutable.DoubleLinkedList
import javax.swing.{JComponent, JSplitPane, JPanel, SpringLayout}
import java.awt.{Dimension, Component}
import java.lang.AssertionError

/**
 * A OneSplit is a class for laying out components in a Row/Column, with at
 * most one splitter. On the left/top side of the splitter we have
 * components places in a row/column inside a JPanel (with a SpringLayout)
 * where on the right/bottom side of the splitter we have another OneSplit.
 *
 * A OneSplit is only valid for display and for adding as a child in other
 * widgets once it has been locked!
 *
 *
 * When there are no following (children) OneSplit's, meaning this OneSplit
 * contains at most one resizeable widget, the OneSplit would look like this:
 *
 * ########################################### <- Root is a JPanel
 * # +---------+ +---------+     +---------+ #
 * # | Widget1 | | Widget2 | ... | WidgetN | #
 * # +---------+ +---------+     +---------+ #
 * ###########################################
 *
 * In case there is also a child OneSplit, it would look like this
 *
 * @@@@@@@@@@@@@@@@@@@@@@@@@@@@ <- Root is a JSplitPane, whose left child
 * @ ####### @                @    is the panel with all the widgets, and
 * @ # ... # @ Child OneSplit @    the right child is the root of the child
 * @ ####### @                @    OneSplit. **The Right-Most Child of the
 * @@@@@@@@@@@@@@@@@@@@@@@@@@@@    JPanel is the resizeable component
 *
 * @param orientation The main orientation of this OneSplit
 * @param parent The parent OneSplit of this container
 */
class OneSplit(val orientation: Orientation, val parent: OneSplit = null) extends HasOrientation {

  /* Import all the orientation utilities */
  import SizingUtils._

  /* Implement the HasOrientation trait */
  def getMainOrientation: Orientation = orientation

  /* Implicit conversion to wrap widgets with the current orientation */
  implicit def orientNativeWidget(nw: NativeWidgetWrapper): OrientedNativeWidgetWrapper = OrientedNativeWidgetWrapper(nw, orientation)

  if (parent != null)
    parent.OSchild = this

  /**
   * The direct child of this OneSplit
   */
  var OSchild: OneSplit = null

  /**
   * A list containing all the child widgets of this OneSplit. These are
   * the children inside the "left" part of the JSplitPane (in case there
   * was a split) or all of the children in case where there was no split
   */
  var children: DoubleLinkedList[NativeWidgetWrapper] = new DoubleLinkedList;

  /**
   * The SpringLayout of the panel
   */
  var layout: SpringLayout = new SpringLayout();

  /**
   * A panel containing all the child widgets that were inserted to this
   * OneSplit before a split (if any) occured
   */
  var panel: JPanel = new JPanel(this.layout) {
    setBorder(null)
  }

  /**
   * In case where this OneSplit was split, this is the JSplitPane which
   * now becomes the root component. It's "left" child would be the panel
   * with all the children and it's "right" child would be the root of the
   * child OneSplit
   */
  var splitPane: JSplitPane = null;

  /**
   * Did we finish adding children and splitting this OneSplit?
   */
  var isLocked: Boolean = false;

  /**
   * Did we glue the end of the last child to the end of the panel
   * already?
   */
  protected var lastGlue: Boolean = false;

  def add(comp: NativeWidgetWrapper) {
    if (isLocked) {
      throw new IllegalStateException("No widgets may be added once"
        + "the OneSplit was locked!");
    } else if (children.contains(comp)) {
      throw new IllegalArgumentException("Can't add the same "
        + "component twice to a MultiSplitPane!");
    }

    val isFirst: Boolean = children.isEmpty;

    panel.add(comp.getNativeWidget);

    /* Positioning along the main axis */
    if (isFirst) {
      /* If this is the first component in the panel, then it's
       * begining should connect to the begining of the panel */
      layout.putConstraint(orientation.springLayoutStart, panel, 0, orientation.springLayoutStart, comp.getNativeWidget);
    } else {
      /* Otherwise it's begining should connect to the end of the
       * previous component */
      layout.putConstraint(orientation.springLayoutStart, comp.getNativeWidget, 0, orientation.springLayoutEnd, children.last.getNativeWidget);
    }

    /* Positioning along the secondary axis */
    if (comp.isFillSecondary) {
      /* If it must fill the secondary axis, then we should connect
       * it's start and end to those of the panel */
      layout.putConstraint(orientation.other.springLayoutStart, comp.getNativeWidget, 0, orientation.other.springLayoutStart, panel);
      layout.putConstraint(orientation.other.springLayoutEnd, comp.getNativeWidget, 0, orientation.other.springLayoutEnd, panel);

    } else {
      /* Otherwise, we just center the component along the secondary
       * axis */
      layout.putConstraint(orientation.other.springLayoutCenter, comp.getNativeWidget, 0, orientation.other.springLayoutCenter, panel);
    }

    /* Appending the widget to the children list */
    children :+= comp
  }

  /**
   * Retrieve the root JComponent of this OneSplit. This is either the
   * panel itself if no splitting occurred, or the JSplitPane (that has the
   * panel as it's left/top child) if splitting occurred
   */
  def getRoot: JComponent = {
    if (!this.isLocked)
      throw new IllegalStateException("The root of a MultiSplitPane "
        + "is not well defined before it was locked! Lock "
        + "before using this component!");

    if (this.splitPane != null) {
      this.splitPane;
    } else {
      this.panel;
    }
  }

  /**
   * Add the JSplitPane (if there was a split) or the JPanel (if there was
   * no split) to the parent OneSplit object, compute it's min/max/pref
   * sizes and also tell it's parents to update their sizes.
   *
   * After this method is called, no more children may be added directly
   * to this OneSplit! However, if there was a child OneSplit created via
   * the split() method, more children CAN be added to that OneSplit.
   */
  def lock() {
    if (this.parent != null && ! this.parent.isLocked) {
      throw new IllegalStateException("Parent OneSplit was not locked while " +
        "trying to lock it's child OneSplit!")
    } else if (this.children.isEmpty) {
      throw new IllegalStateException("OneSplit being locked before any " +
        "children were added to it!")
    } else if (this.isLocked) {
      return
    }

    if (! lastGlue) {
      layout.putConstraint(orientation.springLayoutEnd, panel, 0, orientation.springLayoutEnd, children.last.getNativeWidget);
      lastGlue = true
    }

    this.isLocked = true;

    if (this.parent != null) {
      assert(this.parent.splitPane != null);
      this.parent.splitPane.setRightComponent(this.getRoot);
    }
  }

  /**
   * Create a splitter now (after the last widget that was inserted)
   * @return The OneSplit object that will be on the right side of the
   *         splitter
   */
  def split(): OneSplit = {
    if (children.isEmpty) {
      throw new IllegalStateException("Can't split an empty "
        + "OneSplit! (Actually I can split, but this an "
        + "indication of incorrect usage)");
    } else if (isLocked) {
      throw new IllegalStateException("No widgets may be added once"
        + "the OneSplit was locked!");
    } else {
      /* Create the JSplitPane that should contain the panel */
      this.splitPane = new JSplitPane(orientation.jSplitPaneDirection) {
        setContinuousLayout(true)
        setBorder(null)
        setLeftComponent(panel)
      }

      /* Now, we know exactly what shold be added to the parent, so
       * we should do that. We should also glue the last child to the
       * end.
       */
      lock();

      new OneSplit(orientation, this);
    }
  }

  /**
   * Call when the min/max/pref sizes should be (re-)computed for the
   * entire MultiSplitPane. Generally, you want to call this at the
   * locking of the widget, and any of the children was resized. This
   * method will be invoked automatically
   */
  def refreshSizes(recursive: Boolean = false) {
    /* There is no point at all at calculating the size before the widget is
     * locked! In fact, it would mean nothing! */
    if (! isLocked)
      throw new AssertionError("Calculating a size of a non-locked widget is " +
        "not valid and should not happen!")
    
    val sMinMaxPref = new SizeTriplet()

    for (comp: NativeWidgetWrapper <- children) {
      mutateUsingTriplet(sMinMaxPref, getMinMaxPrefSizes(comp.getNativeWidget), {
        (sSize, compSize) => orientation.copyConcatAlongMe(sSize, compSize)
      })
    }

    setMinMaxPrefSizes(panel, sMinMaxPref.map(oCopy _));

    if (splitPane != null) {
      /* We just validated the OneSplit is locked. So if it has a split pane,
       * then it must have a child inside the right (bottom) side! */
      val right: Component = splitPane.getRightComponent;

      assert (right != null)

      val divSize: Int = splitPane.getDividerSize;

      mutateAllSizes({
        (size: Dimension) => oCopySizeNewMain(size, oGetSize(size) + divSize)
      }, sMinMaxPref);

      mutateUsingTriplet(sMinMaxPref, getMinMaxPrefSizes(right), {
        (sSize, compSize) => orientation.copyConcatAlongMe(sSize, compSize)
      })

      setMinMaxPrefSizes(splitPane, sMinMaxPref.deepCloneTriplet)
    }

    if (parent != null && recursive) {
      parent.refreshSizes(recursive)
    }
  }

  def hasResizeableComponent: Boolean = {
    ! (children filter { (nWidget) => nWidget.isResizeable } isEmpty)
  }

  def getResizeableComponent: NativeWidgetWrapper = {
    var resizeableList = children filter { (nWidget) => nWidget.isResizeable }
    assert(resizeableList.size == 1)
    resizeableList.head
  }

  def getFreeSpace: Int = {
    oGetSize(getResizeableComponent.getNativeWidget.getPreferredSize)
  }

  /**
   * Increase the size along the main axis of the wrapper of widgets directly in
   * this OneSplit. This means that if we are without a splitter, then the size
   * of the JSplitPane would be changed by the given amount. If we have a
   * split-pane, then both it and the panel would be resized.
   */
  def changeWrapperSize(diff: Int) {
    val toResize = Set(panel, getRoot)
    for (compI <- toResize) {
      mutateAllComponentSizes(compI, {
        (size) => oCopySizeNewMain(size, oGetSize(size) + diff)
      })
    }
  }

  /* Increase the amount of "free space" used in this OneSplit. This means that
   * if there is a resizeable component in this OneSplit, it would be increased
   */
  def addFreeSpace(diff: Int) {
    var diffR = diff
    /* First leak only into the children */
    diffR -= addFreeSpace(diffR, true, false)
    /* Now leak the rest into the parents */
    addFreeSpace(diffR, false, true)
  }

  protected def addFreeSpace(diff: Int, canLeakToChild: Boolean, canLeakToParent: Boolean): Int = {
    if (diff == 0)
      return diff
    else if (!hasResizeableComponent) {
      if (parent != null && canLeakToParent)
        return parent.addFreeSpace(diff, false, true)
      else if (OSchild != null && canLeakToChild)
        return parent.addFreeSpace(diff, true, false)
      else
        return diff
    }


    // The current preferred size of the resizeable widget
    val dim: Dimension = new Dimension(getResizeableComponent.getNativeWidget.getPreferredSize)
    // Along the relevant axis
    val size: Int = oGetSize(dim)
    // Prevent the difference we are adding from taking us to negative size
    // values
    var realDiff = if (size + diff > 0) diff else -size

    // If after all that, we have a valid size change, we should now commit it
    if (realDiff != 0) {
      orientation.setSize(dim, size + realDiff)
      getResizeableComponent.getNativeWidget.setPreferredSize(new Dimension (dim))
      getResizeableComponent.getNativeWidget.invalidate()
    }

    // Now, if we haven't managed to propagate all the size difference onwards,
    // we should attempt propagating some of it to the child/parent if possible
    if (realDiff != diff) {
      if (parent != null && canLeakToParent)
        realDiff += parent.addFreeSpace(diff - realDiff, false, true)
      if (realDiff != diff && OSchild != null && canLeakToChild)
        realDiff += OSchild.addFreeSpace(diff - realDiff, true, false)
    }

    realDiff

  }

}